// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Linq;
using System.Threading;
using dnSpy.Roslyn.EditorFeatures.Extensions;
using dnSpy.Roslyn.EditorFeatures.Host;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace dnSpy.Roslyn.EditorFeatures.TextStructureNavigation {
	partial class AbstractTextStructureNavigatorProvider {
		class TextStructureNavigator : ITextStructureNavigator {
			readonly ITextBuffer _subjectBuffer;
			readonly ITextStructureNavigator _naturalLanguageNavigator;
			readonly AbstractTextStructureNavigatorProvider _provider;
			readonly IWaitIndicator _waitIndicator;

			internal TextStructureNavigator(ITextBuffer subjectBuffer,
				ITextStructureNavigator naturalLanguageNavigator,
				AbstractTextStructureNavigatorProvider provider,
				IWaitIndicator waitIndicator) {
				Contract.ThrowIfNull(subjectBuffer);
				Contract.ThrowIfNull(naturalLanguageNavigator);
				Contract.ThrowIfNull(provider);

				_subjectBuffer = subjectBuffer;
				_naturalLanguageNavigator = naturalLanguageNavigator;
				_provider = provider;
				_waitIndicator = waitIndicator;
			}

			public IContentType ContentType => _subjectBuffer.ContentType;

			public TextExtent GetExtentOfWord(SnapshotPoint currentPosition) {
				//using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetExtentOfWord, CancellationToken.None))
				{
					var result = default(TextExtent);
					_waitIndicator.Wait(
						title: EditorFeaturesResources.Text_Navigation,
						message: EditorFeaturesResources.Finding_word_extent,
						allowCancel: true,
						action: waitContext => {
							result = GetExtentOfWordWorker(currentPosition, waitContext.CancellationToken);
						});

					return result;
				}
			}

			TextExtent GetExtentOfWordWorker(SnapshotPoint position, CancellationToken cancellationToken) {
				var textLength = position.Snapshot.Length;
				if (textLength == 0) {
					return _naturalLanguageNavigator.GetExtentOfWord(position);
				}

				// If at the end of the file, go back one character so stuff works
				if (position == textLength && position > 0) {
					position -= 1;
				}

				// If we're at the EOL position, return the line break's extent
				var line = position.Snapshot.GetLineFromPosition(position);
				if (position >= line.End && position < line.EndIncludingLineBreak) {
					return new TextExtent(new SnapshotSpan(line.End, line.EndIncludingLineBreak - line.End),
						isSignificant: false);
				}

				var document = GetDocument(position);
				if (document != null) {
					var root = document.GetSyntaxRootSynchronously(cancellationToken);
					var trivia = root.FindTrivia(position, findInsideTrivia: true);

					if (trivia != default) {
						if (trivia.Span.Start == position && _provider.ShouldSelectEntireTriviaFromStart(trivia)) {
							// We want to select the entire comment
							return new TextExtent(trivia.Span.ToSnapshotSpan(position.Snapshot), isSignificant: true);
						}
					}

					var token = root.FindToken(position, findInsideTrivia: true);

					// If end of file, go back a token
					if (token.Span.Length == 0 && token.Span.Start == textLength) {
						token = token.GetPreviousToken();
					}

					if (token.Span.Length > 0 && token.Span.Contains(position) &&
						!_provider.IsWithinNaturalLanguage(token, position)) {
						// Cursor position is in our domain - handle it.
						return _provider.GetExtentOfWordFromToken(token, position);
					}
				}

				// Fall back to natural language navigator do its thing.
				return _naturalLanguageNavigator.GetExtentOfWord(position);
			}

			public SnapshotSpan GetSpanOfEnclosing(SnapshotSpan activeSpan) {
				//using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfEnclosing, CancellationToken.None))
				{
					var span = default(SnapshotSpan);
					var result = _waitIndicator.Wait(
						title: EditorFeaturesResources.Text_Navigation,
						message: EditorFeaturesResources.Finding_enclosing_span,
						allowCancel: true,
						action: waitContext => {
							span = GetSpanOfEnclosingWorker(activeSpan, waitContext.CancellationToken);
						});

					return result == WaitIndicatorResult.Completed ? span : activeSpan;
				}
			}

			SnapshotSpan GetSpanOfEnclosingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) {
				// Find node that covers the entire span.
				var node = FindLeafNode(activeSpan, cancellationToken);
				if (node != null && activeSpan.Length == node.Value.Span.Length) {
					// Go one level up so the span widens.
					node = GetEnclosingNode(node.Value);
				}

				return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
			}

			public SnapshotSpan GetSpanOfFirstChild(SnapshotSpan activeSpan) {
				//using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfFirstChild, CancellationToken.None))
				{
					var span = default(SnapshotSpan);
					var result = _waitIndicator.Wait(
						title: EditorFeaturesResources.Text_Navigation,
						message: EditorFeaturesResources.Finding_enclosing_span,
						allowCancel: true,
						action: waitContext => {
							span = GetSpanOfFirstChildWorker(activeSpan, waitContext.CancellationToken);
						});

					return result == WaitIndicatorResult.Completed ? span : activeSpan;
				}
			}

			SnapshotSpan GetSpanOfFirstChildWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) {
				// Find node that covers the entire span.
				var node = FindLeafNode(activeSpan, cancellationToken);
				if (node != null) {
					// Take first child if possible, otherwise default to node itself.
					var firstChild = node.Value.ChildNodesAndTokens().FirstOrNull();
					if (firstChild.HasValue) {
						node = firstChild.Value;
					}
				}

				return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
			}

			public SnapshotSpan GetSpanOfNextSibling(SnapshotSpan activeSpan) {
				//using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfNextSibling, CancellationToken.None))
				{
					var span = default(SnapshotSpan);
					var result = _waitIndicator.Wait(
						title: EditorFeaturesResources.Text_Navigation,
						message: EditorFeaturesResources.Finding_span_of_next_sibling,
						allowCancel: true,
						action: waitContext => {
							span = GetSpanOfNextSiblingWorker(activeSpan, waitContext.CancellationToken);
						});

					return result == WaitIndicatorResult.Completed ? span : activeSpan;
				}
			}

			SnapshotSpan GetSpanOfNextSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) {
				// Find node that covers the entire span.
				var node = FindLeafNode(activeSpan, cancellationToken);
				if (node != null) {
					// Get ancestor with a wider span.
					var parent = GetEnclosingNode(node.Value);
					if (parent != null) {
						// Find node immediately after the current in the children collection.
						var nodeOrToken = parent.Value
												.ChildNodesAndTokens()
												.SkipWhile(child => child != node)
												.Skip(1)
												.FirstOrNull();

						if (nodeOrToken.HasValue) {
							node = nodeOrToken.Value;
						}
						else {
							// If this is the last node, move to the parent so that the user can continue
							// navigation at the higher level.
							node = parent.Value;
						}
					}
				}

				return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
			}

			public SnapshotSpan GetSpanOfPreviousSibling(SnapshotSpan activeSpan) {
				//using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfPreviousSibling, CancellationToken.None))
				{
					var span = default(SnapshotSpan);
					var result = _waitIndicator.Wait(
						title: EditorFeaturesResources.Text_Navigation,
						message: EditorFeaturesResources.Finding_span_of_previous_sibling,
						allowCancel: true,
						action: waitContext => {
							span = GetSpanOfPreviousSiblingWorker(activeSpan, waitContext.CancellationToken);
						});

					return result == WaitIndicatorResult.Completed ? span : activeSpan;
				}
			}

			SnapshotSpan GetSpanOfPreviousSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) {
				// Find node that covers the entire span.
				var node = FindLeafNode(activeSpan, cancellationToken);
				if (node != null) {
					// Get ancestor with a wider span.
					var parent = GetEnclosingNode(node.Value);
					if (parent != null) {
						// Find node immediately before the current in the children collection.
						var nodeOrToken = parent.Value
												.ChildNodesAndTokens()
												.Reverse()
												.SkipWhile(child => child != node)
												.Skip(1)
												.FirstOrNull();

						if (nodeOrToken.HasValue) {
							node = nodeOrToken.Value;
						}
						else {
							// If this is the first node, move to the parent so that the user can continue
							// navigation at the higher level.
							node = parent.Value;
						}
					}
				}

				return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
			}

			Document GetDocument(SnapshotPoint point) {
				var textLength = point.Snapshot.Length;
				if (textLength == 0) {
					return null;
				}

				return point.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
			}

			/// <summary>
			/// Finds deepest node that covers given <see cref="SnapshotSpan"/>.
			/// </summary>
			SyntaxNodeOrToken? FindLeafNode(SnapshotSpan span, CancellationToken cancellationToken) {
				if (!TryFindLeafToken(span.Start, out var token, cancellationToken)) {
					return null;
				}

				SyntaxNodeOrToken? node = token;
				while (node != null && (span.End.Position > node.Value.Span.End)) {
					node = GetEnclosingNode(node.Value);
				}

				return node;
			}

			/// <summary>
			/// Given position in a text buffer returns the leaf syntax node it belongs to.
			/// </summary>
			bool TryFindLeafToken(SnapshotPoint point, out SyntaxToken token, CancellationToken cancellationToken) {
				var syntaxTree = GetDocument(point).GetSyntaxTreeSynchronously(cancellationToken);
				if (syntaxTree != null) {
					token = syntaxTree.GetRoot(cancellationToken).FindToken(point, true);
					return true;
				}

				token = default;
				return false;
			}

			/// <summary>
			/// Returns first ancestor of the node which has a span wider than node's span.
			/// If none exist, returns the last available ancestor.
			/// </summary>
			SyntaxNodeOrToken SkipSameSpanParents(SyntaxNodeOrToken node) {
				while (node.Parent != null && node.Parent.Span == node.Span) {
					node = node.Parent;
				}

				return node;
			}

			/// <summary>
			/// Finds node enclosing current from navigation point of view (that is, some immediate ancestors
			/// may be skipped during this process).
			/// </summary>
			SyntaxNodeOrToken? GetEnclosingNode(SyntaxNodeOrToken node) {
				var parent = SkipSameSpanParents(node).Parent;
				if (parent != null) {
					return parent;
				}
				else {
					return null;
				}
			}
		}
	}
}
